/* * Copyright (c) 2005-2016 Vincent Vandenschrick. All rights reserved. * * This file is part of the Jspresso framework. * * Jspresso is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jspresso is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Jspresso. If not, see <http://www.gnu.org/licenses/>. */ package org.jspresso.framework.model.persistence.hibernate.criterion; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import org.hibernate.criterion.CriteriaSpecification; import org.hibernate.criterion.Criterion; import org.hibernate.criterion.DetachedCriteria; import org.hibernate.criterion.Disjunction; import org.hibernate.criterion.Junction; import org.hibernate.criterion.MatchMode; import org.hibernate.criterion.Order; import org.hibernate.criterion.Restrictions; import org.hibernate.sql.JoinType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.jspresso.framework.application.action.AbstractActionContextAware; import org.jspresso.framework.application.backend.action.persistence.hibernate.QueryEntitiesAction; import org.jspresso.framework.model.component.IPropertyTranslation; import org.jspresso.framework.model.component.IQueryComponent; import org.jspresso.framework.model.component.query.ComparableQueryStructure; import org.jspresso.framework.model.component.query.EnumQueryStructure; import org.jspresso.framework.model.component.query.EnumValueQueryStructure; import org.jspresso.framework.model.descriptor.ICollectionPropertyDescriptor; import org.jspresso.framework.model.descriptor.IComponentDescriptor; import org.jspresso.framework.model.descriptor.IEnumerationPropertyDescriptor; import org.jspresso.framework.model.descriptor.IPropertyDescriptor; import org.jspresso.framework.model.descriptor.IReferencePropertyDescriptor; import org.jspresso.framework.model.descriptor.IStringPropertyDescriptor; import org.jspresso.framework.model.descriptor.basic.AbstractComponentDescriptor; import org.jspresso.framework.model.descriptor.query.ComparableQueryStructureDescriptor; import org.jspresso.framework.model.entity.EntityHelper; import org.jspresso.framework.model.entity.IEntity; import org.jspresso.framework.util.bean.PropertyHelper; import org.jspresso.framework.util.collection.ESort; import org.jspresso.framework.view.descriptor.basic.PropertyViewDescriptorHelper; /** * Default implementation of a criteria factory. * * @author Vincent Vandenschrick */ public class DefaultCriteriaFactory extends AbstractActionContextAware implements ICriteriaFactory { private static final Logger LOG = LoggerFactory.getLogger(DefaultCriteriaFactory.class); private boolean triStateBooleanSupported; private boolean useAliasesForJoins; /** * Constructs a new {@code DefaultCriteriaFactory} instance. */ public DefaultCriteriaFactory() { triStateBooleanSupported = false; useAliasesForJoins = false; } /** * {@inheritDoc} */ @SuppressWarnings("ConstantConditions") @Override public void completeCriteriaWithOrdering(EnhancedDetachedCriteria criteria, IQueryComponent queryComponent, Map<String, Object> context) { criteria.setProjection(null); criteria.setResultTransformer(CriteriaSpecification.ROOT_ENTITY); // complete sorting properties if (queryComponent.getOrderingProperties() != null) { for (Map.Entry<String, ESort> orderingProperty : queryComponent.getOrderingProperties().entrySet()) { String propertyName = orderingProperty.getKey(); String[] propElts = propertyName.split("\\."); DetachedCriteria orderingCriteria = criteria; boolean sortable = true; if (propElts.length > 1) { IComponentDescriptor<?> currentCompDesc = queryComponent.getQueryDescriptor(); int i = 0; List<String> path = new ArrayList<>(); for (; sortable && i < propElts.length - 1; i++) { IReferencePropertyDescriptor<?> refPropDescriptor = ((IReferencePropertyDescriptor<?>) currentCompDesc .getPropertyDescriptor(propElts[i])); if (refPropDescriptor != null) { sortable = sortable && isSortable(refPropDescriptor); if (EntityHelper.isInlineComponentReference(refPropDescriptor)) { break; } currentCompDesc = refPropDescriptor.getReferencedDescriptor(); path.add(propElts[i]); } else { LOG.error("Ordering property {} not found on {}", propElts[i], currentCompDesc.getComponentContract().getName()); sortable = false; } } if (sortable) { StringBuilder name = new StringBuilder(); for (int j = i; sortable && j < propElts.length; j++) { IPropertyDescriptor propDescriptor = currentCompDesc.getPropertyDescriptor(propElts[j]); sortable = sortable && isSortable(propDescriptor); if (j < propElts.length - 1) { currentCompDesc = ((IReferencePropertyDescriptor<?>) propDescriptor).getReferencedDescriptor(); } if (j > i) { name.append("."); } name.append(propElts[j]); } if (sortable) { for (String pathElt : path) { if (isUseAliasesForJoins()) { orderingCriteria = criteria.getSubCriteriaFor(orderingCriteria, pathElt, pathElt, JoinType.LEFT_OUTER_JOIN); } else { orderingCriteria = criteria.getSubCriteriaFor(orderingCriteria, pathElt, JoinType.LEFT_OUTER_JOIN); } } propertyName = name.toString(); } } } else { IPropertyDescriptor propertyDescriptor = queryComponent.getQueryDescriptor().getPropertyDescriptor( propertyName); if (propertyDescriptor != null) { sortable = isSortable(propertyDescriptor); } else { LOG.error("Ordering property {} not found on {}", propertyName, queryComponent.getQueryDescriptor().getComponentContract().getName()); sortable = false; } } if (sortable) { Order order; switch (orderingProperty.getValue()) { case DESCENDING: order = Order.desc(PropertyHelper.toJavaBeanPropertyName(propertyName)); break; case ASCENDING: default: order = Order.asc(PropertyHelper.toJavaBeanPropertyName(propertyName)); } orderingCriteria.addOrder(order); } } } // Query should always be ordered to preserve pagination. criteria.addOrder(Order.desc(IEntity.ID)); } private boolean isSortable(IPropertyDescriptor propertyDescriptor) { return propertyDescriptor != null && (!propertyDescriptor.isComputed() || propertyDescriptor.getPersistenceFormula() != null); } /** * {@inheritDoc} */ @Override public EnhancedDetachedCriteria createCriteria(IQueryComponent queryComponent, Map<String, Object> context) { EnhancedDetachedCriteria criteria = EnhancedDetachedCriteria.forEntityName( queryComponent.getQueryContract().getName()); boolean abort = completeCriteria(criteria, criteria, null, queryComponent, context); if (abort) { return null; } return criteria; } @SuppressWarnings("ConstantConditions") private boolean completeCriteria(EnhancedDetachedCriteria rootCriteria, DetachedCriteria currentCriteria, String path, IQueryComponent aQueryComponent, Map<String, Object> context) { boolean abort = false; IComponentDescriptor<?> componentDescriptor = aQueryComponent.getQueryDescriptor(); if (aQueryComponent instanceof ComparableQueryStructure) { completeCriteria(currentCriteria, createComparableQueryStructureRestriction(path, (ComparableQueryStructure) aQueryComponent, componentDescriptor, aQueryComponent, context)); } else { String translationsPath = AbstractComponentDescriptor.getComponentTranslationsDescriptorTemplate().getName(); String translationsAlias = currentCriteria.getAlias() + "_" + componentDescriptor.getComponentContract().getSimpleName() + "_" + translationsPath; if (componentDescriptor.isTranslatable()) { rootCriteria.getSubCriteriaFor(currentCriteria, translationsPath, translationsAlias, JoinType.LEFT_OUTER_JOIN); } for (Map.Entry<String, Object> property : aQueryComponent.entrySet()) { String propertyName = property.getKey(); Object propertyValue = property.getValue(); IPropertyDescriptor propertyDescriptor = componentDescriptor.getPropertyDescriptor(propertyName); if (propertyDescriptor != null) { boolean isEntityRef = false; if (componentDescriptor.isEntity() && aQueryComponent.containsKey(IEntity.ID)) { isEntityRef = true; } if ((!PropertyViewDescriptorHelper.isComputed(componentDescriptor, propertyName) || ( propertyDescriptor instanceof IStringPropertyDescriptor && ((IStringPropertyDescriptor) propertyDescriptor).isTranslatable())) && (!isEntityRef || IEntity.ID .equals(propertyName))) { String prefixedProperty = PropertyHelper.toJavaBeanPropertyName(propertyName); if (path != null) { prefixedProperty = path + "." + prefixedProperty; } if (propertyValue instanceof IEntity) { if (!((IEntity) propertyValue).isPersistent()) { abort = true; } else { completeCriteria(currentCriteria, Restrictions.eq(prefixedProperty, propertyValue)); } } else if (propertyValue instanceof Boolean && (isTriStateBooleanSupported() || (Boolean) propertyValue)) { completeCriteria(currentCriteria, Restrictions.eq(prefixedProperty, propertyValue)); } else if(IEntity.ID.equalsIgnoreCase(propertyName)) { completeCriteria(currentCriteria, createIdRestriction(propertyDescriptor, prefixedProperty, propertyValue, componentDescriptor, aQueryComponent, context)); } else if (propertyValue instanceof String) { completeCriteriaWithTranslations(currentCriteria, translationsPath, translationsAlias, property, propertyDescriptor, prefixedProperty, getBackendController(context).getLocale(), componentDescriptor, aQueryComponent, context); } else if (propertyValue instanceof Number || propertyValue instanceof Date) { completeCriteria(currentCriteria, Restrictions.eq(prefixedProperty, propertyValue)); } else if (propertyValue instanceof EnumQueryStructure) { completeCriteria(currentCriteria, createEnumQueryStructureRestriction(prefixedProperty, ((EnumQueryStructure) propertyValue))); } else if (propertyValue instanceof IQueryComponent) { IQueryComponent joinedComponent = ((IQueryComponent) propertyValue); if (!isQueryComponentEmpty(joinedComponent, propertyDescriptor)) { if (joinedComponent.isInlineComponent()/* || path != null */) { // the joined component is an inline component so we must use // dot nested properties. Same applies if we are in a nested // path i.e. already on an inline component. abort = abort || completeCriteria(rootCriteria, currentCriteria, prefixedProperty, (IQueryComponent) propertyValue, context); } else { // the joined component is an entity so we must use // nested criteria; unless the autoComplete property // is a special char. boolean digDeeper = true; String autoCompleteProperty = joinedComponent.getQueryDescriptor().getAutoCompleteProperty(); if (autoCompleteProperty != null) { String val = (String) joinedComponent.get(autoCompleteProperty); if (val != null) { boolean negate = false; if (val.startsWith(IQueryComponent.NOT_VAL)) { val = val.substring(1); negate = true; } if (IQueryComponent.NULL_VAL.equals(val)) { Criterion crit = Restrictions.isNull(prefixedProperty); if (negate) { crit = Restrictions.not(crit); // there might be other restrictions // digDeeper = false; } else { digDeeper = true; } completeCriteria(currentCriteria, crit); } } } if (digDeeper) { DetachedCriteria joinCriteria; if (isUseAliasesForJoins()) { joinCriteria = rootCriteria.getSubCriteriaFor(currentCriteria, prefixedProperty, prefixedProperty, JoinType.INNER_JOIN); } else { joinCriteria = rootCriteria.getSubCriteriaFor(currentCriteria, prefixedProperty, JoinType.INNER_JOIN); } abort = abort || completeCriteria(rootCriteria, joinCriteria, null, joinedComponent, context); } } } } else if (propertyValue != null) { // Unknown property type. Assume equals. completeCriteria(currentCriteria, Restrictions.eq(prefixedProperty, propertyValue)); } } } } } return abort; } /** * Complete criteria with translations. * * @param currentCriteria * the current criteria * @param translationsPath * the translations path * @param translationsAlias * the translations alias * @param property * the property * @param propertyDescriptor * the property descriptor * @param prefixedProperty * the prefixed property * @param locale * the locale * @param componentDescriptor * the component descriptor * @param queryComponent * the query component * @param context * the context */ @SuppressWarnings("unchecked") protected void completeCriteriaWithTranslations(DetachedCriteria currentCriteria, String translationsPath, String translationsAlias, Map.Entry<String, Object> property, IPropertyDescriptor propertyDescriptor, String prefixedProperty, Locale locale, IComponentDescriptor<?> componentDescriptor, IQueryComponent queryComponent, Map<String, Object> context) { if (propertyDescriptor instanceof IStringPropertyDescriptor && ((IStringPropertyDescriptor) propertyDescriptor) .isTranslatable()) { String nlsOrRawValue = null; String nlsValue = (String) property.getValue(); String barePropertyName = property.getKey(); if (property.getKey().endsWith(IComponentDescriptor.NLS_SUFFIX)) { barePropertyName = barePropertyName.substring(0, barePropertyName.length() - IComponentDescriptor.NLS_SUFFIX.length()); } else { nlsOrRawValue = nlsValue; } if (nlsValue != null) { Junction translationRestriction = Restrictions.conjunction(); translationRestriction.add(createStringRestriction( ((ICollectionPropertyDescriptor<IPropertyTranslation>) componentDescriptor.getPropertyDescriptor( translationsPath)).getCollectionDescriptor().getElementDescriptor().getPropertyDescriptor( IPropertyTranslation.TRANSLATED_VALUE), translationsAlias + "." + IPropertyTranslation.TRANSLATED_VALUE, nlsValue, componentDescriptor, queryComponent, context)); String languagePath = translationsAlias + "." + IPropertyTranslation.LANGUAGE; translationRestriction.add(Restrictions.eq(languagePath, locale.getLanguage())); translationRestriction.add(Restrictions.eq(translationsAlias + "." + IPropertyTranslation.PROPERTY_NAME, barePropertyName)); Junction disjunction = Restrictions.disjunction(); disjunction.add(translationRestriction); if (nlsOrRawValue != null) { Junction rawValueRestriction = Restrictions.conjunction(); rawValueRestriction.add(Restrictions.disjunction().add(Restrictions.isNull(languagePath)).add(Restrictions.ne( languagePath, locale.getLanguage()))); String rawPropertyName = barePropertyName + IComponentDescriptor.RAW_SUFFIX; rawValueRestriction.add(createStringRestriction(componentDescriptor.getPropertyDescriptor(rawPropertyName), rawPropertyName, nlsOrRawValue, componentDescriptor, queryComponent, context)); disjunction.add(rawValueRestriction); } currentCriteria.add(disjunction); } } else { completeCriteria(currentCriteria, createStringRestriction(propertyDescriptor, prefixedProperty, (String) property.getValue(), componentDescriptor, queryComponent, context)); } } /** * Complete with criterion. * * @param currentCriteria * the current criteria * @param criterion * the criterion */ protected void completeCriteria(DetachedCriteria currentCriteria, Criterion criterion) { if (criterion != null) { currentCriteria.add(criterion); } } /** * Complements a criteria by processing an enumeration query structure. * * @param path * the path to the comparable property. * @param enumQueryStructure * the collection of checked / unchecked enumeration values. * @return the created criterion or null if no criterion necessary. */ protected Criterion createEnumQueryStructureRestriction(String path, EnumQueryStructure enumQueryStructure) { Set<String> inListValues = new HashSet<>(); boolean nullAllowed = false; for (EnumValueQueryStructure inListValue : enumQueryStructure.getSelectedEnumerationValues()) { if (inListValue.getValue() == null || "".equals(inListValue.getValue())) { nullAllowed = true; } else { inListValues.add(inListValue.getValue()); } } Junction queryStructureRestriction = null; if (!inListValues.isEmpty()) { queryStructureRestriction = Restrictions.disjunction(); queryStructureRestriction.add(Restrictions.in(path, inListValues)); if (nullAllowed) { queryStructureRestriction.add(Restrictions.isNull(path)); } } return queryStructureRestriction; } /** * Creates an id based restriction. * * @param propertyDescriptor * the id property descriptor. * @param prefixedProperty * the full path of the property. * @param propertyValue * the string property value. * @param componentDescriptor * the component descriptor * @param queryComponent * the query component * @param context * the context * @return the created criterion or null if no criterion necessary. */ @SuppressWarnings("unchecked") protected Criterion createIdRestriction(IPropertyDescriptor propertyDescriptor, String prefixedProperty, Object propertyValue, IComponentDescriptor<?> componentDescriptor, IQueryComponent queryComponent, Map<String, Object> context) { if (propertyValue instanceof Collection<?>) { return QueryEntitiesAction.createEntityIdsInCriterion((Collection<Serializable>) propertyValue, 100); } else { if (propertyValue instanceof String) { return createStringRestriction(propertyDescriptor, prefixedProperty, (String) propertyValue, componentDescriptor, queryComponent, context); } else { return Restrictions.eq(prefixedProperty, propertyValue); } } } /** * Creates a string based restriction. * * @param propertyDescriptor * the property descriptor. * @param prefixedProperty * the full path of the property. * @param propertyValue * the string property value. * @param componentDescriptor * the component descriptor * @param queryComponent * the query component * @param context * the context * @return the created criterion or null if no criterion necessary. */ protected Criterion createStringRestriction(IPropertyDescriptor propertyDescriptor, String prefixedProperty, String propertyValue, IComponentDescriptor<?> componentDescriptor, IQueryComponent queryComponent, Map<String, Object> context) { Junction disjunction = null; if (propertyValue.length() > 0) { String[] stringDisjunctions = propertyValue.split(IQueryComponent.DISJUNCT); disjunction = Restrictions.disjunction(); for (String stringDisjunction : stringDisjunctions) { Junction conjunction = Restrictions.conjunction(); String[] stringConjunctions = stringDisjunction.split(IQueryComponent.CONJUNCT); for (String stringConjunction : stringConjunctions) { String val = stringConjunction; if (val.length() > 0) { Criterion crit; boolean negate = false; if (val.startsWith(IQueryComponent.NOT_VAL)) { val = val.substring(1); negate = true; } if (IQueryComponent.NULL_VAL.equals(val)) { crit = Restrictions.isNull(prefixedProperty); } else { if (IEntity.ID.equals(propertyDescriptor.getName()) || propertyDescriptor instanceof IEnumerationPropertyDescriptor) { crit = Restrictions.eq(prefixedProperty, val); } else { crit = createLikeRestriction(propertyDescriptor, prefixedProperty, val, componentDescriptor, queryComponent, context); } } if (negate) { crit = Restrictions.not(crit); } conjunction.add(crit); } } disjunction.add(conjunction); } } return disjunction; } /** * Creates a like restriction. * * @param propertyDescriptor * the property descriptor. * @param prefixedProperty * the complete property path. * @param propertyValue * the value to create the like restriction for * @param componentDescriptor * the component descriptor * @param queryComponent * the query component * @param context * the context * @return the created criterion or null if no criterion necessary. */ protected Criterion createLikeRestriction(IPropertyDescriptor propertyDescriptor, String prefixedProperty, String propertyValue, IComponentDescriptor<?> componentDescriptor, IQueryComponent queryComponent, Map<String, Object> context) { MatchMode matchMode; if (propertyValue.contains("%")) { matchMode = MatchMode.EXACT; } else { matchMode = MatchMode.START; } if (propertyDescriptor instanceof IStringPropertyDescriptor && ((IStringPropertyDescriptor) propertyDescriptor) .isUpperCase()) { // don't use ignoreCase() to be able to leverage indices. return Restrictions.like(prefixedProperty, propertyValue.toUpperCase(), matchMode); } return Restrictions.like(prefixedProperty, propertyValue, matchMode).ignoreCase(); } /** * Creates a criterion by processing a comparable query structure. * * @param path * the path to the comparable property. * @param queryStructure * the comparable query structure. * @param componentDescriptor * the component descriptor * @param queryComponent * the query component * @param context * the context * @return the created criterion or null if no criterion necessary. */ protected Criterion createComparableQueryStructureRestriction(String path, ComparableQueryStructure queryStructure, IComponentDescriptor<?> componentDescriptor, IQueryComponent queryComponent, Map<String, Object> context) { Junction queryStructureRestriction = null; if (queryStructure.isRestricting()) { queryStructureRestriction = Restrictions.conjunction(); String comparator = queryStructure.getComparator(); Object infValue = queryStructure.getInfValue(); Object supValue = queryStructure.getSupValue(); Object compareValue = infValue; if (compareValue == null) { compareValue = supValue; } switch (comparator) { case ComparableQueryStructureDescriptor.EQ: queryStructureRestriction.add(Restrictions.eq(path, compareValue)); break; case ComparableQueryStructureDescriptor.GT: queryStructureRestriction.add(Restrictions.gt(path, compareValue)); break; case ComparableQueryStructureDescriptor.GE: queryStructureRestriction.add(Restrictions.ge(path, compareValue)); break; case ComparableQueryStructureDescriptor.LT: queryStructureRestriction.add(Restrictions.lt(path, compareValue)); break; case ComparableQueryStructureDescriptor.LE: queryStructureRestriction.add(Restrictions.le(path, compareValue)); break; case ComparableQueryStructureDescriptor.NU: queryStructureRestriction.add(Restrictions.isNull(path)); break; case ComparableQueryStructureDescriptor.NN: queryStructureRestriction.add(Restrictions.isNotNull(path)); break; case ComparableQueryStructureDescriptor.BE: if (infValue != null && supValue != null) { queryStructureRestriction.add(Restrictions.between(path, infValue, supValue)); } else if (infValue != null) { queryStructureRestriction.add(Restrictions.ge(path, infValue)); } else { queryStructureRestriction.add(Restrictions.le(path, supValue)); } break; default: break; } } return queryStructureRestriction; } /** * Whether a query component must be considered empty, thus not generating any * restriction. * * @param queryComponent * the query component to test. * @param holdingPropertyDescriptor * the holding property descriptor or null if none. * @return true, if the query component does not generate any restriction. */ @SuppressWarnings("UnusedParameters") protected boolean isQueryComponentEmpty(IQueryComponent queryComponent, IPropertyDescriptor holdingPropertyDescriptor) { return !queryComponent.isRestricting(); } /** * Gets the triStateBooleanSupported. * * @return the triStateBooleanSupported. */ protected boolean isTriStateBooleanSupported() { return triStateBooleanSupported; } /** * Configures the criteria factory whether to consider use 3-states booleans, i.e. true, false or undefined. If * <strong>triStateBooleanSupported</strong> is set to false, then a {@code false} boolean value will simply be * ignored in the generated criteria. * * @param triStateBooleanSupported * the triStateBooleanSupported to set. */ public void setTriStateBooleanSupported(boolean triStateBooleanSupported) { this.triStateBooleanSupported = triStateBooleanSupported; } /** * Is use aliases for joins. * * @return the boolean */ protected boolean isUseAliasesForJoins() { return useAliasesForJoins; } /** * Sets use aliases for joins. * * @param useAliasesForJoins * the use aliases for joins */ public void setUseAliasesForJoins(boolean useAliasesForJoins) { this.useAliasesForJoins = useAliasesForJoins; } }